/*
 * Copyright (c) 1998, 2011, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.swing.plaf.basic;

import java.io.File;
import java.util.*;
import java.util.concurrent.Callable;
import javax.swing.*;
import javax.swing.filechooser.*;
import javax.swing.event.*;
import java.beans.*;

import sun.awt.shell.ShellFolder;

/**
 * Basic implementation of a file list.
 *
 * @author Jeff Dinkins
 */
public class BasicDirectoryModel extends AbstractListModel<Object> implements
    PropertyChangeListener {

  private JFileChooser filechooser = null;
  // PENDING(jeff) pick the size more sensibly
  private Vector<File> fileCache = new Vector<File>(50);
  private LoadFilesThread loadThread = null;
  private Vector<File> files = null;
  private Vector<File> directories = null;
  private int fetchID = 0;

  private PropertyChangeSupport changeSupport;

  private boolean busy = false;

  public BasicDirectoryModel(JFileChooser filechooser) {
    this.filechooser = filechooser;
    validateFileCache();
  }

  public void propertyChange(PropertyChangeEvent e) {
    String prop = e.getPropertyName();
    if (prop == JFileChooser.DIRECTORY_CHANGED_PROPERTY ||
        prop == JFileChooser.FILE_VIEW_CHANGED_PROPERTY ||
        prop == JFileChooser.FILE_FILTER_CHANGED_PROPERTY ||
        prop == JFileChooser.FILE_HIDING_CHANGED_PROPERTY ||
        prop == JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY) {
      validateFileCache();
    } else if ("UI".equals(prop)) {
      Object old = e.getOldValue();
      if (old instanceof BasicFileChooserUI) {
        BasicFileChooserUI ui = (BasicFileChooserUI) old;
        BasicDirectoryModel model = ui.getModel();
        if (model != null) {
          model.invalidateFileCache();
        }
      }
    } else if ("JFileChooserDialogIsClosingProperty".equals(prop)) {
      invalidateFileCache();
    }
  }

  /**
   * This method is used to interrupt file loading thread.
   */
  public void invalidateFileCache() {
    if (loadThread != null) {
      loadThread.interrupt();
      loadThread.cancelRunnables();
      loadThread = null;
    }
  }

  public Vector<File> getDirectories() {
    synchronized (fileCache) {
      if (directories != null) {
        return directories;
      }
      Vector fls = getFiles();
      return directories;
    }
  }

  public Vector<File> getFiles() {
    synchronized (fileCache) {
      if (files != null) {
        return files;
      }
      files = new Vector<File>();
      directories = new Vector<File>();
      directories.addElement(filechooser.getFileSystemView().createFileObject(
          filechooser.getCurrentDirectory(), "..")
      );

      for (int i = 0; i < getSize(); i++) {
        File f = fileCache.get(i);
        if (filechooser.isTraversable(f)) {
          directories.add(f);
        } else {
          files.add(f);
        }
      }
      return files;
    }
  }

  public void validateFileCache() {
    File currentDirectory = filechooser.getCurrentDirectory();
    if (currentDirectory == null) {
      return;
    }
    if (loadThread != null) {
      loadThread.interrupt();
      loadThread.cancelRunnables();
    }

    setBusy(true, ++fetchID);

    loadThread = new LoadFilesThread(currentDirectory, fetchID);
    loadThread.start();
  }

  /**
   * Renames a file in the underlying file system.
   *
   * @param oldFile a <code>File</code> object representing the existing file
   * @param newFile a <code>File</code> object representing the desired new file name
   * @return <code>true</code> if rename succeeded, otherwise <code>false</code>
   * @since 1.4
   */
  public boolean renameFile(File oldFile, File newFile) {
    synchronized (fileCache) {
      if (oldFile.renameTo(newFile)) {
        validateFileCache();
        return true;
      }
      return false;
    }
  }


  public void fireContentsChanged() {
    // System.out.println("BasicDirectoryModel: firecontentschanged");
    fireContentsChanged(this, 0, getSize() - 1);
  }

  public int getSize() {
    return fileCache.size();
  }

  public boolean contains(Object o) {
    return fileCache.contains(o);
  }

  public int indexOf(Object o) {
    return fileCache.indexOf(o);
  }

  public Object getElementAt(int index) {
    return fileCache.get(index);
  }

  /**
   * Obsolete - not used.
   */
  public void intervalAdded(ListDataEvent e) {
  }

  /**
   * Obsolete - not used.
   */
  public void intervalRemoved(ListDataEvent e) {
  }

  protected void sort(Vector<? extends File> v) {
    ShellFolder.sort(v);
  }

  // Obsolete - not used
  protected boolean lt(File a, File b) {
    // First ignore case when comparing
    int diff = a.getName().toLowerCase().compareTo(b.getName().toLowerCase());
    if (diff != 0) {
      return diff < 0;
    } else {
      // May differ in case (e.g. "mail" vs. "Mail")
      return a.getName().compareTo(b.getName()) < 0;
    }
  }


  class LoadFilesThread extends Thread {

    File currentDirectory = null;
    int fid;
    Vector<DoChangeContents> runnables = new Vector<DoChangeContents>(10);

    public LoadFilesThread(File currentDirectory, int fid) {
      super("Basic L&F File Loading Thread");
      this.currentDirectory = currentDirectory;
      this.fid = fid;
    }

    public void run() {
      run0();
      setBusy(false, fid);
    }

    public void run0() {
      FileSystemView fileSystem = filechooser.getFileSystemView();

      if (isInterrupted()) {
        return;
      }

      File[] list = fileSystem.getFiles(currentDirectory, filechooser.isFileHidingEnabled());

      if (isInterrupted()) {
        return;
      }

      final Vector<File> newFileCache = new Vector<File>();
      Vector<File> newFiles = new Vector<File>();

      // run through the file list, add directories and selectable files to fileCache
      // Note that this block must be OUTSIDE of Invoker thread because of
      // deadlock possibility with custom synchronized FileSystemView
      for (File file : list) {
        if (filechooser.accept(file)) {
          boolean isTraversable = filechooser.isTraversable(file);

          if (isTraversable) {
            newFileCache.addElement(file);
          } else if (filechooser.isFileSelectionEnabled()) {
            newFiles.addElement(file);
          }

          if (isInterrupted()) {
            return;
          }
        }
      }

      // First sort alphabetically by filename
      sort(newFileCache);
      sort(newFiles);

      newFileCache.addAll(newFiles);

      // To avoid loads of synchronizations with Invoker and improve performance we
      // execute the whole block on the COM thread
      DoChangeContents doChangeContents = ShellFolder.invoke(new Callable<DoChangeContents>() {
        public DoChangeContents call() {
          int newSize = newFileCache.size();
          int oldSize = fileCache.size();

          if (newSize > oldSize) {
            //see if interval is added
            int start = oldSize;
            int end = newSize;
            for (int i = 0; i < oldSize; i++) {
              if (!newFileCache.get(i).equals(fileCache.get(i))) {
                start = i;
                for (int j = i; j < newSize; j++) {
                  if (newFileCache.get(j).equals(fileCache.get(i))) {
                    end = j;
                    break;
                  }
                }
                break;
              }
            }
            if (start >= 0 && end > start
                && newFileCache.subList(end, newSize).equals(fileCache.subList(start, oldSize))) {
              if (isInterrupted()) {
                return null;
              }
              return new DoChangeContents(newFileCache.subList(start, end), start, null, 0, fid);
            }
          } else if (newSize < oldSize) {
            //see if interval is removed
            int start = -1;
            int end = -1;
            for (int i = 0; i < newSize; i++) {
              if (!newFileCache.get(i).equals(fileCache.get(i))) {
                start = i;
                end = i + oldSize - newSize;
                break;
              }
            }
            if (start >= 0 && end > start
                && fileCache.subList(end, oldSize).equals(newFileCache.subList(start, newSize))) {
              if (isInterrupted()) {
                return null;
              }
              return new DoChangeContents(null, 0, new Vector(fileCache.subList(start, end)), start,
                  fid);
            }
          }
          if (!fileCache.equals(newFileCache)) {
            if (isInterrupted()) {
              cancelRunnables(runnables);
            }
            return new DoChangeContents(newFileCache, 0, fileCache, 0, fid);
          }
          return null;
        }
      });

      if (doChangeContents != null) {
        runnables.addElement(doChangeContents);
        SwingUtilities.invokeLater(doChangeContents);
      }
    }


    public void cancelRunnables(Vector<DoChangeContents> runnables) {
      for (DoChangeContents runnable : runnables) {
        runnable.cancel();
      }
    }

    public void cancelRunnables() {
      cancelRunnables(runnables);
    }
  }


  /**
   * Adds a PropertyChangeListener to the listener list. The listener is
   * registered for all bound properties of this class.
   * <p>
   * If <code>listener</code> is <code>null</code>,
   * no exception is thrown and no action is performed.
   *
   * @param listener the property change listener to be added
   * @see #removePropertyChangeListener
   * @see #getPropertyChangeListeners
   * @since 1.6
   */
  public void addPropertyChangeListener(PropertyChangeListener listener) {
    if (changeSupport == null) {
      changeSupport = new PropertyChangeSupport(this);
    }
    changeSupport.addPropertyChangeListener(listener);
  }

  /**
   * Removes a PropertyChangeListener from the listener list.
   * <p>
   * If listener is null, no exception is thrown and no action is performed.
   *
   * @param listener the PropertyChangeListener to be removed
   * @see #addPropertyChangeListener
   * @see #getPropertyChangeListeners
   * @since 1.6
   */
  public void removePropertyChangeListener(PropertyChangeListener listener) {
    if (changeSupport != null) {
      changeSupport.removePropertyChangeListener(listener);
    }
  }

  /**
   * Returns an array of all the property change listeners
   * registered on this component.
   *
   * @return all of this component's <code>PropertyChangeListener</code>s or an empty array if no
   * property change listeners are currently registered
   * @see #addPropertyChangeListener
   * @see #removePropertyChangeListener
   * @see java.beans.PropertyChangeSupport#getPropertyChangeListeners
   * @since 1.6
   */
  public PropertyChangeListener[] getPropertyChangeListeners() {
    if (changeSupport == null) {
      return new PropertyChangeListener[0];
    }
    return changeSupport.getPropertyChangeListeners();
  }

  /**
   * Support for reporting bound property changes for boolean properties.
   * This method can be called when a bound property has changed and it will
   * send the appropriate PropertyChangeEvent to any registered
   * PropertyChangeListeners.
   *
   * @param propertyName the property whose value has changed
   * @param oldValue the property's previous value
   * @param newValue the property's new value
   * @since 1.6
   */
  protected void firePropertyChange(String propertyName,
      Object oldValue, Object newValue) {
    if (changeSupport != null) {
      changeSupport.firePropertyChange(propertyName,
          oldValue, newValue);
    }
  }


  /**
   * Set the busy state for the model. The model is considered
   * busy when it is running a separate (interruptable)
   * thread in order to load the contents of a directory.
   */
  private synchronized void setBusy(final boolean busy, int fid) {
    if (fid == fetchID) {
      boolean oldValue = this.busy;
      this.busy = busy;

      if (changeSupport != null && busy != oldValue) {
        SwingUtilities.invokeLater(new Runnable() {
          public void run() {
            firePropertyChange("busy", !busy, busy);
          }
        });
      }
    }
  }


  class DoChangeContents implements Runnable {

    private List<File> addFiles;
    private List<File> remFiles;
    private boolean doFire = true;
    private int fid;
    private int addStart = 0;
    private int remStart = 0;

    public DoChangeContents(List<File> addFiles, int addStart, List<File> remFiles, int remStart,
        int fid) {
      this.addFiles = addFiles;
      this.addStart = addStart;
      this.remFiles = remFiles;
      this.remStart = remStart;
      this.fid = fid;
    }

    synchronized void cancel() {
      doFire = false;
    }

    public synchronized void run() {
      if (fetchID == fid && doFire) {
        int remSize = (remFiles == null) ? 0 : remFiles.size();
        int addSize = (addFiles == null) ? 0 : addFiles.size();
        synchronized (fileCache) {
          if (remSize > 0) {
            fileCache.removeAll(remFiles);
          }
          if (addSize > 0) {
            fileCache.addAll(addStart, addFiles);
          }
          files = null;
          directories = null;
        }
        if (remSize > 0 && addSize == 0) {
          fireIntervalRemoved(BasicDirectoryModel.this, remStart, remStart + remSize - 1);
        } else if (addSize > 0 && remSize == 0 && addStart + addSize <= fileCache.size()) {
          fireIntervalAdded(BasicDirectoryModel.this, addStart, addStart + addSize - 1);
        } else {
          fireContentsChanged();
        }
      }
    }
  }
}
